package filechatserver;

//import com.mysql.jdbc.Statement;
import filechat.Contact;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.*;
import java.util.ArrayList;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;

class FileChatServer implements ObjectStreamListener {

    ServerFrame serverFrame = new ServerFrame();
    WindowListener windowListener = new WindowListener();
    // Variables and arraylists
    //Thread connectionUpdateThread = new checkConnectionsThread();
    static ThreadPoolExecutor executor;
    int currentClient;
    int currentAutClient;
    int number;
    ServerSocket serverSocket;
    int clientNumber = 0;
    int port = 4006;
    int ID;
    int IDOfCurrentFriend;
    int requestType;
    Socket currentSocket;
    String clientData;
    int findFriendID;
    int clientID;
    int currentClientUpdateNumber;
    int currentClientStatusNumber;
    String friendMail;
    String clientMail;
    String clientPassWord;
    String clientIP;
    String clientLocalIP;
    String clientName;
    String[] stringArray;
    String[] clientDataStringArray;
    ArrayList<ObjectOutputStream> clientOutputStreams = new ArrayList();
    ArrayList<ObjectStreamManager> clientManagers = new ArrayList();
    ArrayList<Socket> clientSockets = new ArrayList();
    ArrayList<Integer> clientIDPosition = new ArrayList();
    ArrayList<Integer> clientIDs = new ArrayList();
    ArrayList<Integer> IDsOfFriends = new ArrayList();
    ArrayList<String> clientIPs = new ArrayList();
    ArrayList<Contact> clientsContactList = new ArrayList();
    ArrayList<Contact> currentClientContactList = new ArrayList();
    ArrayList<Contact> currentFriendOfClientsContactList = new ArrayList();
    //Database variables
    Statement s;
    Connection conn;
    ResultSet contactListResultSet;
    ResultSet contactListOfFriend;
    ResultSet authenticationID;
    ResultSet resultSetID;
    //Prepared statements for database queries
    PreparedStatement getContactsStatement = null;
    PreparedStatement setClientStatusAndIPStatement = null;
    PreparedStatement authenticateStatement = null;
    PreparedStatement addFriendStatement = null;
    PreparedStatement getIDStatement = null;

    FileChatServer() {
        serverFrame.addWindowListener(windowListener);
        serverFrame.setVisible(true);
        try {
            openDatabaseConnection();
            startThreadPoolExecutor();
            receiveConnections();
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null,
                    "Couldn't start server: " + e.getMessage(), "Error",
                    JOptionPane.ERROR_MESSAGE);
        }
    }
    //Connects to the database and defines the prepared statements to be used later.

    public void openDatabaseConnection() {
        try {
            Class.forName("com.mysql.jdbc.Driver").newInstance();
            String url = "jdbc:mysql://localhost:3306";
            conn = DriverManager.getConnection(url, "root", "ftcp1234");
            serverFrame.serverTextArea.append("Connected to database\n");
            s = (Statement) conn.createStatement();
            s.executeUpdate("USE mydb3");
            s.close();
            setClientStatusAndIPStatement = conn.prepareStatement("UPDATE ConnectionInformation SET ConnectionStatus=?, IPAdress =? WHERE Client_Index=?");
            getContactsStatement = conn.prepareStatement("SELECT User.Index, User.Username, User.MailAdress, IPAdress, ConnectionStatus FROM ConnectionInformation LEFT JOIN IsFriendsWith ON ConnectionInformation.Client_Index = IsFriendsWith.Client_Index1 LEFT JOIN User ON User.Index = IsFriendsWith.Client_Index1 WHERE IsFriendsWith.Client_Index=?;");
            authenticateStatement = conn.prepareStatement("SELECT User.Index, User.Username FROM User WHERE MailAdress=? AND Password=MD5(?)");
            addFriendStatement = conn.prepareStatement("INSERT INTO IsFriendsWith VALUES ((SELECT User.Index FROM User WHERE MailAdress=?),(SELECT User.Index FROM User WHERE Mailadress=?), 0);");
            getIDStatement = conn.prepareStatement("SELECT User.Index FROM User WHERE MailAdress=?");
        } catch (ClassNotFoundException e) {
            serverFrame.serverTextArea.append(e.getMessage() + "\n");
        } catch (SQLException e) {
            serverFrame.serverTextArea.append(e.getMessage() + "\n");
        } catch (InstantiationException e) {
            serverFrame.serverTextArea.append(e.getMessage() + "\n");
        } catch (IllegalAccessException e) {
            serverFrame.serverTextArea.append(e.getMessage() + "\n");
        }
    }
    // Creates the serversocket and listens for connections, creating streams for all incoming connections and saving the data in arraylists.

    public void receiveConnections() {
        try {
            serverSocket = new ServerSocket(port);

        } catch (Exception e) {
            serverFrame.serverTextArea.append("ERROR: " + e.getMessage() + "\n");
        }
        try {
            while (serverSocket != null) {
                serverFrame.serverTextArea.append("Looking for connections" + "\n");
                Socket socket = serverSocket.accept();
//                if (!connectionUpdateThread.isAlive()) {
//                    connectionUpdateThread.start();
//                }
                ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                ObjectStreamManager manager = new ObjectStreamManager(clientNumber, input, this);
                ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                clientNumber++;
                clientManagers.add(manager);
                clientOutputStreams.add(output);
                clientSockets.add(socket);
            }
        } catch (Exception e) {
            serverFrame.serverTextArea.append("ERROR: " + e.getMessage());
        }
    }

    /*
     * This method starts a ThreadPoolExecutor that is used in the
     * ObjectStreamManager class. The ThreadPoolExecutor handles a number of
     * worker threads (defined in ObjectStreamManager) that in turn handle
     * incoming objects on the streams.
     */
    public static void startThreadPoolExecutor() {
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(100, true);
        executor = new ThreadPoolExecutor(
                50, // core amount of threads
                100, // max amount of threads
                1, // keep alive time
                TimeUnit.MINUTES, // keep alive time unit
                queue);
    }
    /*
     * checks for dead connections and removes them. Retrieves client IP and ID
     * and updates the IP list if the IP is new or changed.
     */

    @Override
    public void objectReceived(int number, Object object, Exception exception) {
        serverFrame.serverTextArea.append("Received Object: " + object + "\n");
        if (exception == null) {
            requestType = checkObject(object);
            if (requestType == 1) {
                clientData = (String) object;
                if (clientData.length() > 2) {
                    System.out.println("Authentification request received");
                    try {
                        if (authenticate(object)) {
                            clientName = authenticationID.getString("Username");
                            //clientOutputStreams.get(number).reset();
                            clientOutputStreams.get(number).writeObject(1 + "\n" + true);
                            clientOutputStreams.get(number).flush();
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException ex) {
                                Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
                            }
                            //clientOutputStreams.get(number).reset();
                            clientOutputStreams.get(number).writeObject(2 + "\n" + clientName);
                            clientOutputStreams.get(number).flush();
                            clientIP = (String) clientSockets.get(number).getInetAddress().getHostAddress();
                            goOnline(clientID, clientIP, number);
                        } else {
                            //clientOutputStreams.get(number).reset();
                            clientOutputStreams.get(number).writeObject(1 + "\n" + false);
                            clientOutputStreams.get(number).flush();
                        }


                    } catch (SQLException ex) {
                        Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (IOException ex) {
                        Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
                    }
                } else {
                }
            } else if (requestType == 2) {
                try {
                    findAndAddFriend(object, number);
                } catch (IOException ex) {
                    Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
                }
            }

        } else {
            serverFrame.serverTextArea.append("Exception in objectReceived: " + exception.getMessage() + "\n");
        }
        for (int i = 0; i < clientSockets.size(); i++) {
                    try {
                        if (clientSockets.get(i).isClosed() || clientSockets.get(i) == null && clientSockets.size() > 0) {
                            System.out.println("Removing connections and going offline");
                            if (clientIDs.size() > 0) {
                                goOffline(clientIDs.get(i));;
                                clientSockets.remove(i);
                                clientOutputStreams.remove(i);
                                clientManagers.remove(i);
                                clientIDs.remove(i);
                                clientIPs.remove(i);
                                clientNumber--;
                                System.out.println("RemovedConnection et al");
                                System.out.println("");
                            } else {
                                break;
                            }
                        }
                    } catch (NullPointerException e) {
                        System.out.println("nullpointer");
                        System.out.println(e.getMessage());
                        //    restartCheckConnectionsThread();
                    } catch (IndexOutOfBoundsException e) {
                        System.out.println("indexoutofbounds");
                        System.out.println(e.getMessage());
                        //    restartCheckConnectionsThread();
                    }
                }

    }
    // Splits the incoming object and checks the type through the first part of the resulting string array

    public int checkObject(Object object) {
        clientData = (String) object;
        clientDataStringArray = clientData.split("\n");
        serverFrame.serverTextArea.append("Object type: " + Integer.parseInt(clientDataStringArray[0]) + " received" + "\n");
        return Integer.parseInt(clientDataStringArray[0]);
    }
    /*
     * This method takes the current client and an object containing that users
     * email and the email of the friend that the user wishes to add. It updates
     * the DB with the new friend connection and updates the contact list of the
     * current client.
     */

    public void findAndAddFriend(Object object, int currentClient) throws IOException {

        clientData = (String) object;
        clientMail = clientData.split("\n")[1];
        friendMail = clientData.split("\n")[2];
        try {
            addFriendStatement.setString(1, clientMail);
            addFriendStatement.setString(2, friendMail);
            addFriendStatement.executeUpdate();

            addFriendStatement.setString(1, friendMail);
            addFriendStatement.setString(2, clientMail);
            addFriendStatement.executeUpdate();

        } catch (SQLException ex) {
            //clientOutputStreams.get(currentClient).reset();
            clientOutputStreams.get(currentClient).writeObject(3 + "\n" + 2);
            clientOutputStreams.get(currentClient).flush();
            serverFrame.serverTextArea.append("Friend add denied\n");
            return;
        }

        try {
            //clientOutputStreams.get(currentClient).reset();
            clientOutputStreams.get(currentClient).writeObject(3 + "\n" + 1);
            clientOutputStreams.get(currentClient).flush();
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
            }

            getIDStatement.setString(1, clientMail);
            resultSetID = getIDStatement.executeQuery();
            resultSetID.next();
            findFriendID = resultSetID.getInt("Index");
            System.out.println(resultSetID.getInt("Index"));

            serverFrame.serverTextArea.append("Friend add accepted\n");
            updateContacts(findFriendID, currentClient);

        } catch (SQLException ex) {
        }
    }

    //Populates an Arraylist with the contact-objects read from the Resultset that was sent from the DB
    public ArrayList writeResultSet(ResultSet resultSet) {
        try {
            if (!clientsContactList.isEmpty()) {
                clientsContactList.clear();
                System.out.println("Cleared clientsContactList ");
            }
            while (resultSet.next()) {
                clientsContactList.add(new Contact(resultSet.getInt("Index"), resultSet.getString("Username"), resultSet.getString("MailAdress"), resultSet.getString("IPAdress"), resultSet.getBoolean("ConnectionStatus")));
                System.out.println(resultSet.getInt("Index") + " " + resultSet.getString("Username") + " " + resultSet.getString("MailAdress") + " " + resultSet.getString("IPAdress") + " " + resultSet.getBoolean("ConnectionStatus"));
            }
        } catch (SQLException e) {
            serverFrame.serverTextArea.append("SQL-ERROR: " + e.getMessage() + "\n");
        }
        return clientsContactList;
    }
    /*
     * Takes the ID and the server-specific number of an incoming request,
     * queries the database for that clients contact list and forwards it to the
     * client.
     */

    public void updateContacts(int ID, int current) {
        try {
            currentClientUpdateNumber = current;
            getContactsStatement.setInt(1, ID);
            contactListResultSet = getContactsStatement.executeQuery();
            System.out.println("contactlsitresultset");
            System.out.println(contactListResultSet);
            currentClientContactList = writeResultSet(contactListResultSet);
            serverFrame.serverTextArea.append("Received Contactlist From DB" + "\n");
        } catch (SQLException ex) {
            Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
        }
        try {
            serverFrame.serverTextArea.append("Size of contactList" + "\n");
            serverFrame.serverTextArea.append(currentClientContactList.size() + "\n");
            System.out.println("currentClientContactList: ");
            System.out.println(currentClientContactList);
            clientOutputStreams.get(currentClientUpdateNumber).reset();
            clientOutputStreams.get(currentClientUpdateNumber).writeObject(currentClientContactList);
            clientOutputStreams.get(currentClientUpdateNumber).flush();
            serverFrame.serverTextArea.append("Sent ContactList" + "\n");
        } catch (Exception ex) {
            Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    /*
     * Takes the ID of the client currently going online or offline and fetches
     * that clients contact through a DB query , queries the DB for the
     * contactlists beloning to the clients contacts and sends them updated to
     * the contacts.
     */

    public void updateContactsOfFriends(int ID) {
        try {
            getContactsStatement.setInt(1, ID);
            contactListResultSet = getContactsStatement.executeQuery();

        } catch (SQLException ex) {
            Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
        }
        try {
            if (!IDsOfFriends.isEmpty()) {
                IDsOfFriends.clear();
            }

            while (contactListResultSet.next()) {
                IDsOfFriends.add(contactListResultSet.getInt("Index"));
            }
            for (int i = 0; i < IDsOfFriends.size(); i++) {
                IDOfCurrentFriend = IDsOfFriends.get(i);
                getContactsStatement.setInt(1, IDOfCurrentFriend);
                contactListOfFriend = getContactsStatement.executeQuery();
                currentFriendOfClientsContactList = writeResultSet(contactListOfFriend);
                System.out.println("Received contact list of friend from DB");

                for (int j = 0; j < clientIDs.size(); j++) {
                    if (IDOfCurrentFriend == clientIDs.get(j) && IDOfCurrentFriend != ID) {
                        serverFrame.serverTextArea.append("Writing updated lists to contacts" + "\n");
                        
                        try {
                            clientOutputStreams.get(j).reset();
                            clientOutputStreams.get(j).writeObject(currentFriendOfClientsContactList);
                            System.out.println("Wrote updated list to contact" + "\n");
                        } catch (IOException ex) {
                            Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
                        }
                    }
                }
            }
        } catch (SQLException ex) {
            Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
        }

    }
    //Updates the DB with the current IP and status of the Client

    public void changeStatusAndIP(int ID, String IP, int status) {
        try {
            setClientStatusAndIPStatement.setInt(1, status);
            setClientStatusAndIPStatement.setString(2, IP);
            setClientStatusAndIPStatement.setInt(3, ID);
            setClientStatusAndIPStatement.executeUpdate();
            //setClientStatusAndIPStatement.close();
            serverFrame.serverTextArea.append("Updated DB with: " + "ID:" + ID + "IP:" + IP + "Status:" + status + "\n");

        } catch (SQLException ex) {
            Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
        }

    }
    /*
     * Queries the DB for the clients ID using the Email and the Password. If an
     * ID is returned the client is authenticated and the ID saved otherwise the
     * method returns false.
     */

    public boolean authenticate(Object object) throws SQLException {
        clientData = (String) object;

        clientMail = clientData.split("\n")[1];
        clientPassWord = clientData.split("\n")[2];

        authenticateStatement.setString(1, clientMail);
        authenticateStatement.setString(2, clientPassWord);
        authenticationID = authenticateStatement.executeQuery();

        if (authenticationID.next()) {
            authenticationID.first();
            clientID = authenticationID.getInt("Index");
            System.out.println("true!");
            return true;
        } else {
            return false;
        }

    }
    /*
     * Both these methods Invoke appropriate methods for going online and
     * offline. They are called from the objectReceived method and the
     * CheckConnection method respectively.
     */

    public void goOnline(int ID, String IP, int current) {
        clientIDs.add(ID);
        clientIPs.add(IP);
        System.out.println("Changing status and IP");
        changeStatusAndIP(ID, IP, 1);
        System.out.println("Updating Contacts");
        updateContacts(ID, current);
        System.out.println("Updating friends");
        updateContactsOfFriends(ID);
        serverFrame.serverTextArea.append("Client with" + "ID:" + ID + "and IP:" + IP + "Local IP:" + clientLocalIP + "/n" + " went online." + "\n");
    }

    public void goOffline(int ID) {
        changeStatusAndIP(ID, null, 0);
        updateContactsOfFriends(ID);
        serverFrame.serverTextArea.append("Client with ID:" + ID + " went offline." + "\n");
    }

    public void close() throws SQLException {
        serverFrame.serverTextArea.append("Trying to close" + "\n");

        try {
            if (serverSocket != null) {
                serverSocket.close();
                serverFrame.serverTextArea.append("ServerSocketClosed" + "\n");
            }
            for (int k = 0; k < clientSockets.size(); k++) {
                if (clientSockets.get(k) != null) {
                    clientSockets.get(k).close();
                    serverFrame.serverTextArea.append("ClientSocketClosed" + "\n");
                }
            }
            for (int k = 0; k < clientManagers.size(); k++) {
                if (clientManagers.get(k) != null) {
                    clientManagers.get(k).closeManager();
                    serverFrame.serverTextArea.append("ManagerClosed" + "\n");
                }
            }
            for (int k = 0; k < clientOutputStreams.size(); k++) {
                if (clientOutputStreams.get(k) != null) {
                    clientOutputStreams.get(k).close();
                    serverFrame.serverTextArea.append("StreamClosed" + "\n");

                }
            }
            conn.close();
        } catch (IOException e) {
            serverFrame.serverTextArea.append("Error in closing: " + e.getMessage() + "\n");
        }
    }
    //Restarts a thread that checks for dead connection if it has been terminated.

    public void restartCheckConnectionsThread() {
        if (!connectionUpdateThread.isAlive()) {
            System.out.println("restarted checkconnectionsthread");
            connectionUpdateThread.start();
        }
    }

    /**
     * This is a thread that is started when the first connection is received.
     * It constantly checks for dead connections and, upon finding them, invokes
     * the goOffline method to update the DB and then removes the connection
     * from all affected lists.
     */
//    private class checkConnectionsThread extends Thread {
//
//        @Override
//        public void run() {
//            System.out.println("Running checkConnectionThread");
//            while (serverSocket != null) {
//                for (int i = 0; i < clientSockets.size(); i++) {
//                    try {
//                        if (clientSockets.get(i).isClosed() || clientSockets.get(i) == null && clientSockets.size() > 0) {
//                            System.out.println("Removing connections and going offline");
//                            if (clientIDs.size() > 0) {
//                                goOffline(clientIDs.get(i));;
//                                clientSockets.remove(i);
//                                clientOutputStreams.remove(i);
//                                clientManagers.remove(i);
//                                clientIDs.remove(i);
//                                clientIPs.remove(i);
//                                clientNumber--;
//                                System.out.println("RemovedConnection et al");
//                                System.out.println("");
//                            } else {
//                                break;
//                            }
//                        }
//                    } catch (NullPointerException e) {
//                        System.out.println("nullpointer");
//                        System.out.println(e.getMessage());
//                        //    restartCheckConnectionsThread();
//                    } catch (IndexOutOfBoundsException e) {
//                        System.out.println("indexoutofbounds");
//                        System.out.println(e.getMessage());
//                        //    restartCheckConnectionsThread();
//                    }
//                }
//            }
//        }
//    }
    /*
     * Listener-Class that uses method windowClosing() to close the server
     * properly on windowClosing operation...
     */

    class WindowListener extends WindowAdapter {

        @Override
        public void windowClosing(WindowEvent e) {
            try {
                close();
            } catch (SQLException ex) {
                Logger.getLogger(FileChatServer.class.getName()).log(Level.SEVERE, null, ex);
            }
            System.exit(0);
        }
    }
}
